/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { ArkFile, ts } from "arkanalyzer";
import Logger, { LOG_MODULE_TYPE } from 'arkanalyzer/lib/utils/logger';
import { FileMatcher, MatcherCallback, MatcherTypes } from "../../matcher/Matchers";
import { Defects, IssueReport } from "../../model/Defects";
import { Rule } from "../../model/Rule";
import { BaseChecker, BaseMetaData } from "../BaseChecker";
import { RuleListUtil } from "../../utils/common/DefectsList";

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'NoCondAssignCheck');
const gMetaData: BaseMetaData = {
  severity: 2,
  ruleDocPath: "docs/no-cond-assign-check.md",
  description: "Disallow assignment operators in conditional expressions.",
};

export class NoCondAssignCheck implements BaseChecker {
  public defects: Defects[] = [];
  public issues: IssueReport[] = [];
  readonly metaData: BaseMetaData = gMetaData;
  public rule: Rule;

  private defualtOption: string = "except-parens";
  private option: string = this.defualtOption;

  private fileMatcher: FileMatcher = {
    matcherType: MatcherTypes.FILE,
  };

  public registerMatchers(): MatcherCallback[] {
    const matchFileCb: MatcherCallback = {
      matcher: this.fileMatcher,
      callback: this.check
    }
    return [matchFileCb];
  }

  /**
   * 判断 TypeScript 代码中条件表达式的合法性，并返回不合法的行列号和错误信息
   * @param code 要检查的 TypeScript 代码
   * @param mode 检查模式，"always" 表示任何赋值都是错误的，其他模式表示仅当赋值括在括号中时才允许
   * @returns 包含不合法条件表达式位置和错误信息的对象数组
   */
  private checkConditionValidity(code: string, mode: string): { line: number, character: number, message: string }[] {
    const sourceFile = ts.createSourceFile('temp.ts', code, ts.ScriptTarget.Latest, true);
    const invalidPositions: { line: number, character: number, message: string }[] = [];

    function checkNode(node: ts.Node) {
      if (ts.isIfStatement(node) || ts.isForStatement(node) || ts.isWhileStatement(node) || ts.isDoStatement(node)) {
        let condition;
        if (ts.isIfStatement(node) || ts.isWhileStatement(node) || ts.isDoStatement(node)) {
          condition = node.expression;
        } else if (ts.isForStatement(node)) {
          condition = node.condition;
        }
        if (!condition) {
          return;
        }

        if (mode === "always" && containsEqualsSign(condition)) {
          const { line, character } = sourceFile.getLineAndCharacterOfPosition(condition.getStart());
          invalidPositions.push({
            line: line + 1,
            character: character + 1,
            message: `Unexpected assignment within a '${getConditionalType(node)}' statement.`
          });
        } else if (ts.isBinaryExpression(condition) && condition.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
          const { line, character } = sourceFile.getLineAndCharacterOfPosition(condition.getStart());
          const parent = condition.parent;
          if (!ts.isParenthesizedExpression(parent)) {
            invalidPositions.push({
              line: line + 1,
              character: character + 1,
              message: 'Expected a conditional expression and instead saw an assignment.'
            });
          }
        }
      }
      ts.forEachChild(node, checkNode);
    }

    function containsEqualsSign(condition: ts.Node): boolean {
      const conditionText = condition.getText();
      const equalsRegex = /(?<![=!])=(?!=)/;
      return equalsRegex.test(conditionText);
    }

    function getConditionalType(node: ts.Node): string {
      if (ts.isIfStatement(node)) return 'if';
      if (ts.isForStatement(node)) return 'for';
      if (ts.isWhileStatement(node)) return 'while';
      if (ts.isDoStatement(node)) return 'do...while';
      return 'unknown';
    }

    checkNode(sourceFile);
    return invalidPositions;
  }

  public check = (targetField: ArkFile) => {
    const severity = this.rule.alert ?? this.metaData.severity;
    let code = targetField.getCode();
    if (this.rule.option && this.rule.option && this.rule.option[0]) {
      this.option = this.rule.option[0] as string;
    }
    const myInvalidPositions = this.checkConditionValidity(code, this.option);
    myInvalidPositions.forEach(pos => {
      this.addIssueReport(pos, severity, targetField.getFilePath());
    });
  }
    
  private addIssueReport(pos: { line: number, character: number, message: string }, severity: number, filePath: string) {
    const fixKey = pos.line + '%' + pos.character + '%' + this.rule.ruleId;
    const mergeKey = filePath + '%' + fixKey + '%' + pos.message;
    let defects = new Defects(pos.line, pos.character, pos.message, severity, this.rule.ruleId,
      mergeKey, this.metaData.ruleDocPath, true, false, true, fixKey);
    this.issues.push(new IssueReport(defects, undefined));
    RuleListUtil.push(defects);
  }
}
